package com.luych.toolbox.data.service.service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.luych.toolbox.common.enums.DataProjectStatus;
import com.luych.toolbox.common.enums.DataProjectTimeLineType;
import com.luych.toolbox.common.enums.DataUpgradeStatus;
import com.luych.toolbox.common.enums.DataUpgradeType;
import com.luych.toolbox.common.feign.intf.base.service.UserFeign;
import com.luych.toolbox.common.feign.intf.gateway.center.FileFeign;
import com.luych.toolbox.common.feign.param.data.service.DataProjectInitParam;
import com.luych.toolbox.common.feign.param.data.service.DataProjectParam;
import com.luych.toolbox.common.feign.param.data.service.DataProjectUpgradeParam;
import com.luych.toolbox.common.feign.view.base.service.UserView;
import com.luych.toolbox.common.feign.view.data.service.DataProjectTimeLineItemView;
import com.luych.toolbox.common.feign.view.data.service.DataProjectTimeLineView;
import com.luych.toolbox.common.feign.view.data.service.DataProjectView;
import com.luych.toolbox.common.feign.view.data.service.DataSourceView;
import com.luych.toolbox.common.page.PageParam;
import com.luych.toolbox.common.page.PageResult;
import com.luych.toolbox.common.service.base.BaseConvert;
import com.luych.toolbox.common.service.base.BaseService;
import com.luych.toolbox.common.service.exception.HttpException;
import com.luych.toolbox.common.service.page.SpringPageParamAdapter;
import com.luych.toolbox.common.service.page.SpringPageResultAdapter;
import com.luych.toolbox.common.service.security.SecurityUtil;
import com.luych.toolbox.data.service.constant.Constant;
import com.luych.toolbox.data.service.dao.*;
import com.luych.toolbox.data.service.entity.*;
import com.luych.toolbox.data.service.job.DataProjectUpgradeJob;
import org.quartz.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;

@Service
public class DataProjectService
        extends BaseService<DataProjectView, DataProjectParam, DataProject>{
    private static final Logger LOGGER = LoggerFactory.getLogger(DataProjectService.class);

    private static final BaseConvert<DataProjectParam, DataProject> dataProjectParamToEntity = DataProject::new;
    private static final BaseConvert<DataProject, DataProjectView> dataProjectEntityToView = DataProjectView::new;
    private static final BaseConvert<DataSource, DataSourceView> dataSourceEntityToView = DataSourceView::new;

    private final ObjectMapper objectMapper;
    private final Scheduler scheduler;

    private final TransactionTemplate transactionTemplate;

    private final DataProjectDao dataProjectDao;
    private final DataSourceDao dataSourceDao;
    private final DataAgentDao dataAgentDao;
    private final DataProjectRDataSourceDao dataProjectRDataSourceDao;
    private final DataUpgradeDao dataUpgradeDao;
    private final DataUpgradeDetailDao dataUpgradeDetailDao;

    private final UserFeign userFeign;
    private final FileFeign fileFeign;

    @Autowired
    public DataProjectService(ObjectMapper objectMapper,
                              Scheduler scheduler,
                              TransactionTemplate transactionTemplate,
                              DataProjectDao dataProjectDao,
                              DataSourceDao dataSourceDao,
                              DataAgentDao dataAgentDao,
                              DataProjectRDataSourceDao dataProjectRDataSourceDao,
                              DataUpgradeDao dataUpgradeDao,
                              DataUpgradeDetailDao dataUpgradeDetailDao,
                              UserFeign userFeign,
                              FileFeign fileFeign) {
        super(dataProjectDao, dataProjectParamToEntity, dataProjectEntityToView);
        this.objectMapper = objectMapper;
        this.scheduler = scheduler;
        this.transactionTemplate = transactionTemplate;
        this.dataProjectDao = dataProjectDao;
        this.dataSourceDao = dataSourceDao;
        this.dataAgentDao = dataAgentDao;
        this.dataProjectRDataSourceDao = dataProjectRDataSourceDao;
        this.dataUpgradeDao = dataUpgradeDao;
        this.dataUpgradeDetailDao = dataUpgradeDetailDao;
        this.userFeign = userFeign;
        this.fileFeign = fileFeign;
    }

    @Override
    protected DataProjectView fill(DataProjectView view) {
        view.setDataSources(new ArrayList<>());
        List<DataProjectRDataSource> dataProjectRDataSources = dataProjectRDataSourceDao.findByDataProjectId(view.getId());
        for (DataProjectRDataSource dataProjectRDataSource : dataProjectRDataSources) {
            Optional<DataSource> dataSource = dataSourceDao.findById(dataProjectRDataSource.getDataSourceId());
            dataSource.map(dataSourceEntityToView).ifPresent(view.getDataSources()::add);
        }
        return view;
    }

    @Override
    public DataProjectView save(DataProjectParam param) {
        DataProject dataProjectParam = dataProjectParamToEntity.apply(param);
        DataProject dataProjectResult = dataProjectDao.save(dataProjectParam);

        List<String> newDsIds = param.getDataSourceIds();
        List<String> oldDsIds = dataProjectRDataSourceDao.findByDataProjectId(dataProjectResult.getId())
                .stream().map(DataProjectRDataSource::getDataSourceId).collect(Collectors.toList());

        for (String newDsId : newDsIds) {
            if (oldDsIds.contains(newDsId)) continue;
            DataProjectRDataSource dataProjectRDataSource = new DataProjectRDataSource();
            dataProjectRDataSource.setId(UUID.randomUUID().toString());
            dataProjectRDataSource.setDataProjectId(dataProjectResult.getId());
            dataProjectRDataSource.setDataSourceId(newDsId);
            dataProjectRDataSourceDao.save(dataProjectRDataSource);
        }

        for (String oldDsId : oldDsIds) {
            if (newDsIds.contains(oldDsId)) continue;
            dataProjectRDataSourceDao.deleteByDataProjectIdAndDataSourceId(dataProjectResult.getId(), oldDsId);
        }

        return this.fill(dataProjectEntityToView.apply(dataProjectResult));
    }

    @Override
    @Transactional
    public DataProjectView create(DataProjectParam param) {
        Assert.notNull(param.getName(), "Param name can't be empty");
        Assert.notEmpty(param.getDataSourceIds(), "Param dataSourceIds can't be empty");

        // check name exists
        if (dataProjectDao.findByName(param.getName()).isPresent()) {
            throw new HttpException(HttpStatus.CONFLICT, Constant.DATA_PROJECT_SAVE_NAME_USED);
        }

        // save
        param.setStatus(DataProjectStatus.UNINITIALIZED);
        return this.save(param);
    }

    @Override
    @Transactional
    public DataProjectView modify(DataProjectParam param) {
        Assert.notNull(param.getId(), "Param id can't be empty");
        Assert.notNull(param.getName(), "Param name can't be empty");
        Assert.notEmpty(param.getDataSourceIds(), "Param dataSourceIds can't be empty");

        // get old data
        DataProject old = dataProjectDao.findById(param.getId()).orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND, Constant.DATA_PROJECT_NOT_FOUND)
        );

        // check name exists
        Optional<DataProject> dataProjectOptOfName = dataProjectDao.findByName(param.getName());
        if (dataProjectOptOfName.isPresent() && !dataProjectOptOfName.get().getId().equals(param.getId())) {
            throw new HttpException(HttpStatus.CONFLICT, Constant.DATA_PROJECT_SAVE_NAME_USED);
        }

        // save
        param.setStatus(old.getStatus());
        return this.save(param);
    }

    @Override
    @Transactional
    public void remove(String id) {
        dataUpgradeDetailDao.deleteByDataProjectId(id);
        dataUpgradeDao.deleteByDataProjectId(id);
        dataProjectRDataSourceDao.deleteByDataProjectId(id);
        dataProjectDao.deleteById(id);
    }

    public void setEnable(String id) {
        // TODO: 2023/2/13
        DataProject dataProject = dataProjectDao.findById(id).orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND, Constant.DATA_PROJECT_NOT_FOUND));

        if (!DataProjectStatus.DISABLE.equals(dataProject.getStatus())) {
            throw new HttpException(HttpStatus.FORBIDDEN, Constant.DATA_PROJECT_ENABLE_STATUS_ERROR);
        }

        dataProject.setStatus(DataProjectStatus.ENABLE);
        dataProjectDao.save(dataProject);
    }

    public void setDisable(String id) {
        // TODO: 2023/2/13
        DataProject dataProject = dataProjectDao.findById(id).orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND, Constant.DATA_PROJECT_NOT_FOUND));

        if (!DataProjectStatus.ENABLE.equals(dataProject.getStatus())) {
            throw new HttpException(HttpStatus.FORBIDDEN, Constant.DATA_PROJECT_DISABLE_STATUS_ERROR);
        }

        dataProject.setStatus(DataProjectStatus.DISABLE);
        dataProjectDao.save(dataProject);
    }

    public void doInit(String id, DataProjectInitParam param) {
        DataProject dataProject = dataProjectDao.findById(id).orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND, Constant.DATA_PROJECT_NOT_FOUND));

        if (!DataProjectStatus.UNINITIALIZED.equals(dataProject.getStatus())) {
            throw new HttpException(HttpStatus.FORBIDDEN, Constant.DATA_PROJECT_INIT_STATUS_ERROR);
        }

        UserView session = SecurityUtil.getSession();

        DataUpgrade dataUpgrade = new DataUpgrade();
        dataUpgrade.setId(UUID.randomUUID().toString());
        dataUpgrade.setDataProjectId(dataProject.getId());
        dataUpgrade.setName("初始化");
        dataUpgrade.setType(DataUpgradeType.INIT);
        dataUpgrade.setOwnerId(session.getId());
        dataUpgrade.setStatus(DataUpgradeStatus.LOADING);

        List<DataUpgradeDetail> dataUpgradeDetails = new ArrayList<>();
        for (DataProjectInitParam.Detail detail : param.getDetails()) {
            DataUpgradeDetail dataUpgradeDetail = new DataUpgradeDetail();
            dataUpgradeDetail.setId(UUID.randomUUID().toString());
            dataUpgradeDetail.setDataUpgradeId(dataUpgrade.getId());
            dataUpgradeDetail.setDataProjectId(dataProject.getId());
            dataUpgradeDetail.setDataSourceId(detail.getDataSourceId());
            dataUpgradeDetail.setDataAgentId(detail.getDataAgentId());
            dataUpgradeDetail.setScript(detail.getInitFileId());
            dataUpgradeDetail.setStatus(DataUpgradeStatus.LOADING);
            dataUpgradeDetails.add(dataUpgradeDetail);
        }

        transactionTemplate.executeWithoutResult(status -> {
            dataUpgradeDao.save(dataUpgrade);
            for (DataUpgradeDetail dataUpgradeDetail : dataUpgradeDetails) {
                dataUpgradeDetailDao.save(dataUpgradeDetail);
            }
        });

        for (DataProjectInitParam.Detail detail : param.getDetails()) {
            fileFeign.confirm(detail.getInitFileId());
        }

        Trigger jobTrigger = this.getJobTrigger(dataUpgrade.getId());
        JobDetail jobDetail = this.getJobDetail(dataUpgrade.getId());
        try {
            scheduler.scheduleJob(jobDetail, jobTrigger);
        } catch (SchedulerException e) {
            throw new IllegalStateException(e);
        }
    }

    public void doUpgrade(String id, DataProjectUpgradeParam param) {
        DataProject dataProject = dataProjectDao.findById(id).orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND, Constant.DATA_PROJECT_NOT_FOUND));

        if (!DataProjectStatus.ENABLE.equals(dataProject.getStatus())) {
            throw new HttpException(HttpStatus.FORBIDDEN, Constant.DATA_PROJECT_UPGRADE_STATUS_ERROR);
        }

        UserView session = SecurityUtil.getSession();

        DataUpgrade dataUpgrade = new DataUpgrade();
        dataUpgrade.setId(UUID.randomUUID().toString());
        dataUpgrade.setDataProjectId(dataProject.getId());
        dataUpgrade.setName(param.getName());
        dataUpgrade.setType(DataUpgradeType.UPGRADE);
        dataUpgrade.setOwnerId(session.getId());
        dataUpgrade.setStatus(DataUpgradeStatus.LOADING);

        List<DataUpgradeDetail> dataUpgradeDetails = new ArrayList<>();
        for (DataProjectUpgradeParam.Detail detail : param.getDetails()) {
            DataUpgradeDetail dataUpgradeDetail = new DataUpgradeDetail();
            dataUpgradeDetail.setId(UUID.randomUUID().toString());
            dataUpgradeDetail.setDataUpgradeId(dataUpgrade.getId());
            dataUpgradeDetail.setDataProjectId(dataProject.getId());
            dataUpgradeDetail.setDataSourceId(detail.getDataSourceId());
            dataUpgradeDetail.setDataAgentId(detail.getDataAgentId());
            dataUpgradeDetail.setScript(detail.getSql());
            dataUpgradeDetail.setStatus(DataUpgradeStatus.LOADING);
            dataUpgradeDetails.add(dataUpgradeDetail);
        }

        transactionTemplate.executeWithoutResult(status -> {
            dataUpgradeDao.save(dataUpgrade);
            for (DataUpgradeDetail dataUpgradeDetail : dataUpgradeDetails) {
                dataUpgradeDetailDao.save(dataUpgradeDetail);
            }
        });

        if (param.getRecordOnly()) {
            transactionTemplate.executeWithoutResult(status -> {
                dataUpgrade.setStartDate(new Date());
                dataUpgrade.setCompleteDate(new Date());
                dataUpgrade.setStatus(DataUpgradeStatus.SUCCESS);
                dataUpgradeDao.save(dataUpgrade);

                for (DataUpgradeDetail dataUpgradeDetail : dataUpgradeDetails) {
                    dataUpgradeDetail.setStartDate(new Date());
                    dataUpgradeDetail.setCompleteDate(new Date());
                    dataUpgradeDetail.setStatus(DataUpgradeStatus.SUCCESS);
                    dataUpgradeDetailDao.save(dataUpgradeDetail);
                }
            });
        } else {
            Trigger jobTrigger = this.getJobTrigger(dataUpgrade.getId());
            JobDetail jobDetail = this.getJobDetail(dataUpgrade.getId());
            try {
                scheduler.scheduleJob(jobDetail, jobTrigger);
            } catch (SchedulerException e) {
                throw new IllegalStateException(e);
            }
        }
    }

    public PageResult<DataProjectView> pageByKeyword(PageParam pageParam, String keyword) {
        Page<DataProject> page;
        if (StringUtils.isEmpty(keyword)) {
            page = dataProjectDao.findAll(new SpringPageParamAdapter(pageParam));
        } else {
            page = dataProjectDao.findByNameLike('%' + keyword + '%', new SpringPageParamAdapter(pageParam));
        }
        PageResult<DataProject> pageResult = new SpringPageResultAdapter<>(page);
        return pageResult.convert(dataProjectEntityToView).convert(this::fill);
    }

    private JobKey getJobKey(String id) {
        return new JobKey(id, DataProjectUpgradeJob.class.getName());
    }

    private JobDetail getJobDetail(String id) {
        Map<String, Object> jobParam = new HashMap<String, Object>() {{
            this.put("id", id);
        }};
        return JobBuilder.newJob(DataProjectUpgradeJob.class)
                .withIdentity(this.getJobKey(id))
                .usingJobData(new JobDataMap(jobParam))
                .build();
    }

    private Trigger getJobTrigger(String id) {
        return TriggerBuilder.newTrigger()
                .withIdentity(id, DataProjectUpgradeJob.class.getName())
                .withSchedule(SimpleScheduleBuilder.repeatSecondlyForTotalCount(1, 1))
                .build();
    }

    private String getUserNameByUserId(String userId) {
        UserView userView = userFeign.get(userId);
        return userView == null ? Constant.USER_DELETED : userView.getUserName();
    }

    public List<DataProjectTimeLineView> timelines(String id) throws JsonProcessingException {
        Assert.notNull(id, "Param id can't be empty");

        DataProject dataProject = dataProjectDao.findById(id).orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND, Constant.DATA_PROJECT_NOT_FOUND));
        List<DataSource> dataSources = new ArrayList<>();
        for (DataProjectRDataSource dpds : dataProjectRDataSourceDao.findByDataProjectId(dataProject.getId())) {
            dataSources.add(dataSourceDao.findById(dpds.getDataSourceId()).orElseThrow(() ->
                    new HttpException(HttpStatus.NOT_FOUND, Constant.DATA_SOURCE_NOT_FOUND)));
        }

        Map<String, List<DataProjectTimeLineItemView>> timelineMap = new TreeMap<>();
        SimpleDateFormat dataFormat = new SimpleDateFormat("yyyy-MM-dd");
        SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm:ss.SSS");
        // create
        List<Map<String, String>> createItemData1 = dataSources.stream().map(ds -> new HashMap<String, String>() {{
            this.put("name", ds.getName());
            this.put("typeName", ds.getType().name());
            this.put("typeValue", ds.getType().getValue());
        }}).collect(Collectors.toList());
        DataProjectTimeLineItemView createItem = new DataProjectTimeLineItemView();
        createItem.setType(DataProjectTimeLineType.CREATE);
        createItem.setTime(timeFormat.format(dataProject.getCreateDate()));
        createItem.setUser(this.getUserNameByUserId(dataProject.getCreateUserId()));
        createItem.setContent(dataProject.getName());
        createItem.setData1(objectMapper.writeValueAsString(createItemData1));
        createItem.setResult(true);

        timelineMap.putIfAbsent(dataFormat.format(dataProject.getCreateDate()), new ArrayList<>());
        timelineMap.get(dataFormat.format(dataProject.getCreateDate())).add(createItem);
        // upgrade
        List<DataUpgrade> dataUpgrades = dataUpgradeDao.findByDataProjectId(dataProject.getId());
        for (DataUpgrade dataUpgrade : dataUpgrades) {
            List<Map<String, String>> upgradeItemdata1 = new ArrayList<>();
            List<DataUpgradeDetail> upgradeDetails = dataUpgradeDetailDao.findByDataUpgradeId(dataUpgrade.getId());
            for (DataUpgradeDetail upgradeDetail : upgradeDetails) {
                DataSource dataSource = dataSourceDao.findById(upgradeDetail.getDataSourceId()).orElseThrow(() ->
                        new HttpException(HttpStatus.NOT_FOUND, Constant.DATA_SOURCE_NOT_FOUND));
                upgradeItemdata1.add(new HashMap<String, String>(){{
                    this.put("name", dataSource.getName());
                    this.put("typeName", dataSource.getType().name());
                    this.put("typeValue", dataSource.getType().getValue());
                    this.put("script", upgradeDetail.getScript());
                    this.put("statusName", upgradeDetail.getStatus().name());
                    this.put("statusValue", upgradeDetail.getStatus().getValue());
                    this.put("errorMessage", upgradeDetail.getErrorMessage());
                }});
            }

            DataProjectTimeLineItemView recoveryItem = new DataProjectTimeLineItemView();
            if (DataUpgradeType.INIT.equals(dataUpgrade.getType())) {
                recoveryItem.setType(DataProjectTimeLineType.INIT);
                recoveryItem.setContent(String.format("数据项目[%s]初始化", dataProject.getName()));
            } else if (DataUpgradeType.UPGRADE.equals(dataUpgrade.getType())) {
                recoveryItem.setType(DataProjectTimeLineType.UPGRADE);
                recoveryItem.setContent(dataUpgrade.getName());
            }
            recoveryItem.setTime(timeFormat.format(dataUpgrade.getStartDate()));
            recoveryItem.setUser(this.getUserNameByUserId(dataUpgrade.getOwnerId()));
            recoveryItem.setResult(DataUpgradeStatus.SUCCESS.equals(dataUpgrade.getStatus()));
            recoveryItem.setData1(objectMapper.writeValueAsString(upgradeItemdata1));

            timelineMap.putIfAbsent(dataFormat.format(dataUpgrade.getStartDate()), new ArrayList<>());
            timelineMap.get(dataFormat.format(dataUpgrade.getStartDate())).add(recoveryItem);
        }
        // sort
        List<DataProjectTimeLineView> timelines = new ArrayList<>();
        for (Map.Entry<String, List<DataProjectTimeLineItemView>> entry : timelineMap.entrySet()) {
            entry.getValue().sort(Comparator.comparing(DataProjectTimeLineItemView::getTime));

            DataProjectTimeLineView timeline = new DataProjectTimeLineView();
            timeline.setDate(entry.getKey());
            timeline.setItems(entry.getValue());
            timelines.add(timeline);
        }

        // num
        int num = 1;
        for (DataProjectTimeLineView timeLine : timelines) {
            for (DataProjectTimeLineItemView timeItem : timeLine.getItems()) {
                timeItem.setNum(num);
                num++;
            }
        }

        return timelines;
    }

}
